iT邦幫忙

2024 iThome 鐵人賽

DAY 10
0
Software Development

從零開始構建能理解語義的 Linebot 架構系列 第 10

使用 AWS Lambda 開發 Serverless Event 接收器: Node.js LINE Bot Handler

  • 分享至 

  • xImage
  •  

概述

在這篇文章中,我們將探討如何在AWS Lambda上運行一個基於Node.js的 LINE Bot Handler,並解析其技術原理與最佳實踐。首先,我們將概述AWS Lambda的執行生命周期,並介紹官方定義的Lambda Function格式。隨後,我們會詳細檢視事件處理邏輯及Kafka Producer的程式結構。

AWS Lambda 執行環境與生命週期

  • 一個Lambda Function被執行時會經過四個步驟,如下圖:
    https://ithelp.ithome.com.tw/upload/images/20240924/2010522781uJGKljhe.png
  • 1.把要執行的Code下載到AWS內部的S3
  • 2.準備執行環境,包括佈署虛擬機器
  • 3.執行初始化程式碼,載入環境變數並建立資料庫連線等必要設置。
  • 4.執行handler function的主程式碼。

冷啟動與熱啟動的差異

冷啟動 (Cold Start)

  • 冷啟動是Lambda在第一次執行,或長時間未執行後,必須重新初始化執行環境的過程。在冷啟動時,Lambda 會需要執行上述的全部四個步驟。
  • 冷啟動會增加初次請求的延遲,特別是對於大型函數或需要大量依賴的應用程式。
    為了減少冷啟動的影響,可以優化初始化代碼,盡可能減少不必要的外部依賴的載入。

熱啟動 (Warm Start)

  • 在Lambda每次執行完後,會將執行環境凍結,並在一段不確定的時間內保留。
  • 熱啟動 (Warm Start) 則是 Lambda Function已經被執行過,執行環境還在保留時間內的啟動方式。
    在這種情況下只需直接執行handler function,執行時間較短,因為省略了重複的初始化過程。

Node.js Lambda function handler

以下Lambda Function用於處理 LINE bot Webhook,並將提取的訊息內容回覆訊息所需的Token傳送到 Kafka。

程式基本結構及進入點

一個 Lambda Function 必須遵循 AWS 官方規範的檔案和函數名稱。根據使用的程式語言,這些規範會有所不同。例如,對於Java 17,程式碼必須包含一個類別HandlerIntegerJava17,並在其中定義 handleRequest函數。

在本專案中,我們使用的是 Node.js,因此必須有一個名為index.jsindex.mjs的檔案,並在其中定義一個handler function。其基本結構如下:

export const handler = async (event, context) => {
  // 取得AWS Lambda event內容。
  // event 是一個 JSON 字串,可以從中解析出 LINE Platform 傳送來的資訊。
  const bodyContent = JSON.parse(body);
  
  return context.logStreamName;
};

程式結構

.env
.eslintrc.json
deploy.sh
env.example
index.js
package.json
utils/kafka/producer.js

使用到的Library

  • @line/bot-sdk: 用於直接回覆LINE使用者的訊息。
  • aws-xray-sdk: 此為將Node.js的程式部署到AWS Lambda上執行的必要依賴
  • dotenv: 用於加載環境變數。
  • kafkajs: 用於將使用者的訊息及Token寫入Kafka Message Queue,供其他程式做進一步的處理。
  • eslint: 用於程式碼格式檢查。
  • eslint-config-airbnb-base: 本專案使用Airbnb 的 ESLint規則。
  • eslint-plugin-import: 用於支持 ES6 模塊的 ESLint 插件。

環境變數

.env 文件中包含了以下環境變數:

# 要連接的Kafka Host IP 或 Domain Name
KAFKA_HOST_IP=
# 要將訊息送到哪一個Kafka Topic
KAFKA_TOPIC=TEST-MESSAGE-TOPIC
# 要連接的Kafka Port, 預設為9092
KAFKA_PORT=
# 要連接的Client id
KAFKA_CLIENT_ID=
# 指定LINE BOT 的Channel Access Token
CHANNEL_ACCESS_TOKEN=
  • Channel Access Token的取得方式可參考這篇的說明

部署

使用上一篇的deploy.sh腳本進行部署:

function=LinebotHandlerNode ./deploy.sh

主要程式碼說明

index.js

  • AWS Lambda Function入口,負責處理來自LINE Bot的事件,並將訊息發送至Kafka。
  • 分離事件處理邏輯與 Lambda Handler 定義: 為了讓程式更具可測試性,我們將處理事件的邏輯與 Lambda Handler 函數的宣告分離。這樣的設計便於撰寫單元測試,也提高程式的可維護性。
  • Handler Factory模式: 因為除了本專案以外,當時還有其他Hanlder Function在測試其他功能,常常需要切換。因此我們使用了簡單的 Handler Factory,根據輸入的key決定要export哪一個Handler給AWS Lambda,方便抽換和測試。
  • Event結構: 這邊是使用Function URL作為接收端,Event的結構和API Gateway相同,而event.body的內容就是LINE Platform送過來的Webhook。
  • 有關LINE Platform Webhook的結構,可參考先前文章中提到的Webhook 與 事件(Event)
const { produceMessage } = require('./utils/kafka/producer');
require('dotenv').config();

const kafkaHandler = async (event, context) => {
  const kafkaTopic = process.env.KAFKA_TOPIC;
  const { body } = event;
  const bodyContent = JSON.parse(body);
  const { events } = bodyContent;
  const produceKafkaResults = [];
  events.forEach((e) => {
    if (e.type === 'message' && e.message !== undefined && e.message.type === 'text') {
      const {
        source: {
          userId,
        },
        message: {
          text,
        },
        timestamp,
        replyToken,
      } = e;

      produceKafkaResults.push(produceMessage({
        topic: kafkaTopic,
        messages: [{ key: userId, value: text }],
      }));
    }
  });
  await Promise.all(produceKafkaResults);
  return context.logStreamName;
};

// 簡單的handler Factory,當需要開發多個Handler function需要切換時,較為方便
const handlerFactory = (name) => {
  const functionCode = {
    kafka: kafkaHandler,
    other: otherHandler,
  };

  return functionCode[name];
};

// 將Lambda Handler與主要邏輯分開,方便測試及抽換
exports.handler = async function (event, context) {
  const handler = handlerFactory('kafka');
  await handler(event, context);
};

utils/kafka/producer.js

  • Kafka Producer,提供一個Function: produceMessage(), 用於將訊息傳遞到 Kafka。
  • produceMessage: 這邊先從環境變數取得需要的Kafka Host, Port, 以及要指定的Topic等連線資訊。最後將指定的訊息送到對應的Topic。
  • 有關於Kafka Message Queue的說明將在後續的章節詳盡描述。
const {
  Kafka, logLevel, CompressionTypes, Partitioners,
} = require('kafkajs');
require('dotenv').config();

const host = process.env.KAFKA_HOST_IP || 'localhost';
const port = process.env.KAFKA_PORT || 9092;
const clientId = process.env.KAFKA_CLIENT_ID || 'example-producer';

const kafka = new Kafka({
  logLevel: logLevel.DEBUG,
  brokers: [`${host}:${port}`],
  clientId,
});

const producer = kafka.producer({
  allowAutoTopicCreation: true,
  createPartitioner: Partitioners.LegacyPartitioner,
});

const produceMessage = async ({ topic = 'NON-GIVEN-MESSAGE', messages = [] } = {}) => {
  try {
    await producer.connect(); 
    await producer.send({
      topic,
      compression: CompressionTypes.GZIP,
      messages,
    });
  } catch (e) {
    console.error(`[example/producer] ${e.message}`, e);
  } finally {
    await producer.disconnect();
  }
};

module.exports = {
  produceMessage,
};

AWS Lambda Node.js Best Practices

以下說明幾個官方提到的AWS Lambda最佳實踐:

  • 善用Initial Code階段
    • 宣告Function handler以外的部分將會在Initial Code被執行,也就是說這部分的Code只會在第一次調用,或者Lambda Function重新被初始化時才會執行。
    • 所以盡可能把資料庫連線,或者載入依賴(require(...))的程式碼擺在Function handler,可簡化每次的執行時間
  • 保持無狀態: Lambda 函數應該是無狀態的,這樣可以確保水平擴展的彈性。
  • 日誌記錄CloudWatch: 使用 console.log來記錄的日誌,將自動發送到 Amazon CloudWatch Logs,可以在AWS Lambda 主控台找到這個 Lambda Function所在的Log:
    https://ithelp.ithome.com.tw/upload/images/20240923/20105227aZEHobRG5f.png

總結

以上介紹的 Node.js Lambda Function 能夠解析從 LINE Platform 傳送過來的 Webhook,並從中提取使用者的訊息內容、Token 及其他相關資訊。

由於我們希望整個系統由各司其職的小型服務組成,在這裡,我們不直接對取得的資訊做後續處理,取而代之的是,透過Kafka這個Message Queue,將訊息傳遞給後端的 Bot Server,交給它來做後續的語意判斷及資料儲存等工作。

Kafka作為多個小型服務彼此溝通的橋樑,其運作原理以及如何設定,我們將在後面的章節詳細介紹。

Citation

https://docs.aws.amazon.com/lambda/latest/operatorguide/execution-environments.html
https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html#nodejs-best-practices


上一篇
使用 AWS Lambda 開發 Serverless Event 接收器: 建立IAM Account / 使用 AWS CLI進行部署
下一篇
Kafka 概念介紹及部署: 主要角色及基本觀念
系列文
從零開始構建能理解語義的 Linebot 架構30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言